<?php
/**
 * Geneone: Content Management Framework
 * Authentication Layer
 *
 * Builds on top of PEAR::Auth
 *
 * http://geneone.neondragon.net/
 *
 * @package Geneone
 * @author Khlo
 * @version 1.0
 * @copyright 2004-2006 Geneone Development Team
 */
 
define ('GENE_AUTH_THROTTLE_NONE', 0);
define ('GENE_AUTH_THROTTLE_CAPTCHA', 1);
define ('GENE_AUTH_THROTTLE_BLOCKED', 2);

class Gene_Auth extends Auth {
	/**
	 * Class Constructor.
	 *
	 * @see Auth::Auth()
	 */
	function Gene_Auth($storage, $options='', $loginFunction='', $showlogin=true) {
		$this->Auth($storage, $options, $loginFunction, $showlogin);

		if (!$this->getAuth()) {
			/* Persistant Login */
	        $agent = Gene_Request::getString("autologinagent", GENE_REQUEST_COOKIE);
	        $key =  Gene_Request::getString("autologinkey", GENE_REQUEST_COOKIE);
	        
	        if ($agent && $key) {
		        $db =& Gene::getDatabase();
		        
		        $q = $db->prepare("SELECT * FROM ".Gene::tableName("auto_login")." WHERE agent_id=? AND loginkey=?");
		        $res = $q->execute(array($agent, $key));
		        
	        	if ($q = $res->fetchRow()) {
		        	$this->username = $q['user_id'];
					
		        	srand(microtime() * 100000);
					$this->session['challengekey'] = md5($this->username.rand(0,10000));
					$this->setAuth($this->username);
					
		            if (is_callable($this->loginCallback)) {
		                call_user_func_array($this->loginCallback, array($this->username, &$this) );
		            }
		            
		            $this->session['lastpwauth'] = false;
		            
		            /* Create a new login key for the next session */
		            $newloginkey = md5($this->username.time().rand(0,10000).$this->session['challengekey']);
		            
		            $db->extended->autoExecute(
			            Gene::tableName("auto_login"), 
			            array(
							"loginkey" => $newloginkey,
							"timestamp" => time(),
						), 
						MDB2_AUTOQUERY_UPDATE,
						"agent_id=".$db->quote($agent)
					);
		            
					Gene_Request::setCookie("autologinkey", $newloginkey);
					
					return true;
	        	} else {
		        	return false;
	        	}
	    	}
    	}
    	
    	/* Periodically also generate a new auto login key for next session. */
    	srand(microtime() * 100000);
    	if (Gene_Request::getString("autologinagent", GENE_REQUEST_COOKIE) && rand(0,50) == 50) {
	    	$agent = Gene_Request::getString("autologinagent", GENE_REQUEST_COOKIE);
	    	
	    	/* Create a new login key for the next session */
            $newloginkey = md5($this->username.time().rand(0,10000).$this->session['challengekey']);
            $db =& Gene::getDatabase();
            $db->extended->autoExecute(
	            Gene::tableName("auto_login"),
	            array(
					"loginkey" => $newloginkey,
					"timestamp" => time(),
				),
				MDB2_AUTOQUERY_UPDATE,
				"agent_id=".$db->quote($agent)
			);
			
			Gene_Request::setCookie("autologinkey", $newloginkey);
    	}
	}
	
	/**
	 * Returns user's login status
	 *
	 * @see Auth::getAuth()
	 */
	function loggedIn() {
		return $this->getAuth();
	}
	
	/**
	 * Returns Gene_User object instance for logged in member.
	 *
	 * @return object Instance of Gene_User
	 */
	function getUser() {
		return new Gene_User($_SESSION['_authsession']['username']);
	}
	
	/**
	 * Return number of failed logins by IP address in last hour.
	 */
	function getThrottleCount() {
		static $throttle = null;
		
		if ($throttle === null) {
			$db =& Gene::getDatabase();
					
			$q = $db->prepare("SELECT SUM(failedcount) AS c FROM ".Gene::tableName("login_throttle")." WHERE ip_address=? AND timestamp > ?");
			$res = $q->execute(array($_SERVER['REMOTE_ADDR'], time()-3600));
			$throttle = $res->fetchOne();
			$res->free();
		}
		
		return $throttle;	
	}
	
	/**
	 * Returns throttle level.
	 */
	function getThrottleLevel($attempts=null) {
		static $status = null;
		
		if ($attempts !== null || $status === null) {
			if (Gene::getSetting("login_throttle")) {
				if ($attempts === null) {
					$attempts = $this->getThrottleCount();
				}
				
				if ($attempts >= Gene::getSetting("login_throttle_block")) {
					$status = GENE_AUTH_THROTTLE_BLOCKED;
				} else if (Gene::getSetting("login_throttle_captcha") && $attempts >= Gene::getSetting("login_throttle_captcha")) {
					$status = GENE_AUTH_THROTTLE_CAPTCHA;
				} else {
					$status = GENE_AUTH_THROTTLE_NONE;
				}
			} else {
				$status = GENE_AUTH_THROTTLE_NONE;
			}
		}
		
		return $status;
	}
	
	/**
	 * Returns number of failed logins on user in the last hour.
	 */
	function getUserThrottleCount($userId) {
		static $throttle = array();
		
		if (!isset($throttle[$userId])) {
			$db =& Gene::getDatabase();
					
			$q = $db->prepare("SELECT SUM(failedcount) AS c FROM ".Gene::tableName("login_throttle")." WHERE user_id=? AND timestamp > ?");
			$res = $q->execute(array($userId, time()-3600));
			$throttle[$userId] = $res->fetchOne();
			$res->free();
		}
		
		return $throttle[$userId];	
	}
	
	/**
	 * Returns user throttle level.
	 */
	function getUserThrottleLevel($userId, $attempts=null) {
		static $status = array();
		
		if ($attempts !== null || !isset($status[$userId])) {
			if (Gene::getSetting("login_throttle_user")) {
				if ($attempts === null) {
					$attempts = $this->getUserThrottleCount($userId);
				}
				
				if (Gene::getSetting("login_throttle_captcha") && $attempts >= Gene::getSetting("login_throttle_user_captcha")) {
					$status[$userId] = GENE_AUTH_THROTTLE_CAPTCHA;
				} else {
					$status[$userId] = GENE_AUTH_THROTTLE_NONE;
				}
			} else {
				$status[$userId] = GENE_AUTH_THROTTLE_NONE;
			}
		}
		
		if (isset($status[$userId])) {
			return $status[$userId];
		}
	}
	
	/**
	 * Bump throttle status
	 */
	function bumpThrottle($userId) {
		$db =& Gene::getDatabase();
		
		if ($this->getThrottleCount()) {
			$q = $db->prepare ("SELECT failedcount, timestamp FROM ".Gene::tableName("login_throttle")." WHERE ip_address=? AND user_id=? AND timestamp > ?");
			$res = $q->execute(array($_SERVER['REMOTE_ADDR'], $userId, time()-3600));
			$row = $res->fetchRow();
			
			if ($row) {
				$q = $db->prepare ("UPDATE ".Gene::tableName("login_throttle")." SET failedcount=?, timestamp=? WHERE ip_address=? AND user_id=? AND timestamp=?");
				$q->execute(array($row['failedcount']+1, time(), $_SERVER['REMOTE_ADDR'], $userId, $row['timestamp']));
			} else {
				$db->extended->autoExecute(
					Gene::tableName("login_throttle"),
					array(
						"ip_address" => $_SERVER['REMOTE_ADDR'],
						"user_id" => $userId,
						"failedcount" => 1,
						"timestamp" => time(),
					),
					MDB2_AUTOQUERY_INSERT
				);
			}
		} else {
			$db->extended->autoExecute(
				Gene::tableName("login_throttle"),
				array(
					"ip_address" => $_SERVER['REMOTE_ADDR'],
					"user_id" => $userId,
					"failedcount" => 1,
					"timestamp" => time(),
				),
				MDB2_AUTOQUERY_INSERT
			);
		}
		
		// Garbage Collection
		if (rand(0,50) == 50) {
			$q = $db->prepare ("DELETE FROM ".Gene::tableName("login_throttle")." WHERE timestamp < ?");
			$q->execute(array(time()-3600));
		}
	}
	
	/**
	 * Login the current user.
	 */
	function doLogin($loginname, $password, $persistant=false) {
		$db =& Gene::getDatabase();
		
		if ($this->getThrottleLevel() == GENE_AUTH_THROTTLE_BLOCKED) {
			return PEAR::raiseError("Logins from your IP address have been temporarily blocked due to abnormal usage. Please try again later.");
		}
		
		$user = false;
		
		if ($loginname AND $password) {
			$user = Gene_User::fromPublicName($loginname);
			if ($user) {
				$this->username = $user->getUserId();
				$this->password = $password;
				
				if (!$this->checkAuth() && $this->allowLogin) {
					$this->login();
				}
			} else {
				return PEAR::raiseError("We could not login with the provided login name and password. Please ensure you have entered them correctly.");
			}
		}
		
		// Check login status
		if ($this->getAuth()) {
			Gene_Session::updateSessData(session_id());
			if ($persistant) {
				srand(microtime() * 100000);
				
				$time = time();
				$agentkey = md5($user->getUserId().$loginname.time().rand(0,1000).$password);
				$loginkey = md5($user->getUserId().$loginname.time().rand(0,1000).$password);

				$db->extended->autoExecute(
					Gene::tableName("auto_login"),
					array(
						"agent_id" => $agentkey,
						"user_id" => $user->getUserId(),
						"loginkey" => $loginkey,
						"timestamp" => $time,
					),
					MDB2_AUTOQUERY_INSERT
				);
				
				Gene_Request::setCookie("autologinagent", $agentkey);
				Gene_Request::setCookie("autologinkey", $loginkey);
			} else {
				Gene_Request::setCookie("autologinagent", "");
				Gene_Request::setCookie("autologinkey", "");
			}
			
			$this->session['lastpwauth'] = time();
			return true;
		} else {
			if (Gene::getSetting("login_throttle") && $user) {
				$this->bumpThrottle($user->getUserId());
			}
			
			return PEAR::raiseError("We could not login with the provided login name and password. Please ensure you have entered them correctly.");
		}
	}
	
    /**
     * Has the user been authenticated?
     *
     * @access public
     * @return bool  True if the user is logged in, otherwise false.
     */
    function getAuth() {
        return $this->checkAuth();
    }
	
    /**
     * Register variable in a session telling that the user
     * has logged in successfully
     *
     * @param  string Username
     * @return void
     * @access public
     */
    function setAuth($username) {
        // #2021 - Change the session id to avoid session fixation attacks php 4.3.3 > 
        session_regenerate_id();
       	Gene_Session::gc();

        if (!isset($this->session) || !is_array($this->session)) {
            $this->session = array();
        }

        if (!isset($this->session['data'])) {
            $this->session['data'] = array();
        }

        $this->session['sessionip'] = isset($this->server['REMOTE_ADDR']) 
            ? $this->server['REMOTE_ADDR'] 
            : '';
        $this->session['sessionuseragent'] = isset($this->server['HTTP_USER_AGENT']) 
            ? $this->server['HTTP_USER_AGENT'] 
            : '';

        // This should be set by the container to something more safe
        // Like md5(passwd.microtime)
        if(empty($this->session['challengekey'])) {
            $this->session['challengekey'] = md5($username.microtime());
        }

        $this->session['challengecookie'] = md5($this->session['challengekey'].microtime());
        
        // Modified to use Gene cookie code
        Gene_Request::setCookie("authchallenge", $this->session['challengecookie'], "session");

        $this->session['registered'] = true;
        $this->session['username']   = $username;
        $this->session['timestamp']  = time();
        $this->session['idle']       = time();
    }
	
	/**
	 * Logout the current user.
	 */
	function logout() {
		/* Remove any persistant login cookies */
		Gene_Request::setCookie("autologinagent", "");
		Gene_Request::setCookie("autologinkey", "");
		
		/* Remove persistant login DB entry */
        if (Gene_Request::getString("autologinagent", GENE_REQUEST_COOKIE)) {
	        $db =& Gene::getDatabase();
	        $q = $db->prepare ("DELETE FROM ".Gene::tableName("auto_login")." WHERE agent_id=?");
	        $res = $q->execute(Gene_Request::getString("autologinagent", GENE_REQUEST_COOKIE));
        }
				
		parent::logout();
		session_destroy();
		return true;
	}
	
    /**
     * Add user to the storage container
     *
     * @access public
     * @param  string Username
     * @param  string Password
     * @param  mixed  Additional parameters
     * @return mixed  True on success, PEAR error object on error
     *                and AUTH_METHOD_NOT_SUPPORTED otherwise.
     */
	function addUser($username, $password, $additional='') {
		trigger_error("Use Gene_User::newUser();", E_USER_ERROR);
		return false;
	}
	
	/**
	 * Pages which make big changes (user account changes, admin, etc.) should
	 * call this function. It will tell you whether the user should be asked
	 * to retype their password before they are allowed to continue with
	 * what they want to do.
	 *
	 * @return boolean Whether passwords needs to be retyped
	 */
	function requirePasswordRetype() {
		if (Gene::getSetting("sess_password_retype")) {
			$cutoff = time()-(Gene::getSetting("sess_password_retype") * 60);
			if ($cutoff > $this->session['lastpwauth']) {
				return true;
			}
		}
		
		return false;
	}
	
	/**
	 * If the user has retyped their password, call this function.
	 */
	function bumpPasswordRetype() {
		$this->session['lastpwauth'] = time();
	}
}
?>